import cache from 'utils/cache';
import Process from 'process/process';
import {getRoomIntel} from 'room-intel';
import {isCrossroads} from 'utils/room-name';

declare global {
	interface StrategyMemory {
		caravans?: Record<string, {
			creeps: Array<Id<Creep>>;
			dir: TOP | BOTTOM | LEFT | RIGHT;
			firstSeen: number;
			expires: number;
			rooms: Array<{
				name: string;
				time: number;
			}>;
			contents: Partial<Record<ResourceConstant, number>>;
		}>;
	}
}

export default class HighwayRoomProcess extends Process {
	room: Room;

	/**
	 * Manages rooms we own.
	 * @constructor
	 *
	 * @param {object} parameters
	 *   Options on how to run this process.
	 */
	constructor(parameters: RoomProcessParameters) {
		super(parameters);
		this.room = parameters.room;
	}

	/**
	 * Manages one of our rooms.
	 */
	run() {
		this.detectCaravans();
		this.pruneOldCaravans();
	}

	detectCaravans() {
		for (const creep of this.room.enemyCreeps[SYSTEM_USERNAME] || []) {
			const id = this.getCaravanId(creep);
			if (!id) continue;

			this.registerCaravan(id);
		}
	}

	getCaravanId(creep: Creep): string {
		if (!creep.name.includes('_', creep.name.length - 2)) return null;

		return creep.name.slice(0, Math.max(0, creep.name.length - 2));
	}

	registerCaravan(id: string) {
		if (!Memory.strategy) Memory.strategy = {};
		if (!Memory.strategy.caravans) Memory.strategy.caravans = {};

		const creeps = _.sortBy(_.filter(this.room.enemyCreeps[SYSTEM_USERNAME], c => c.name.startsWith(id)), c => c.name);
		if (Memory.strategy.caravans[id] && creeps.length < Memory.strategy.caravans[id].creeps.length) {
			// Don't update info about caravans that are already registered if we
			// can't see all previously known creeps.
			return;
		}

		const direction = this.detectDirection(creeps);
		if (!direction) return;

		const firstSeen = Memory.strategy.caravans[id]?.firstSeen || Game.time;
		const rooms = this.getTraversedRooms(direction, creeps, firstSeen);

		Memory.strategy.caravans[id] = {
			firstSeen,
			creeps: _.map<Creep, Id<Creep>>(creeps, 'id'),
			dir: direction,
			expires: rooms[rooms.length - 1].time + 50,
			rooms,
			contents: this.getStoreContents(creeps),
		};
	}

	detectDirection(creeps: Creep[]): TOP | BOTTOM | LEFT | RIGHT {
		const minX = _.min(creeps, c => c.pos.x);
		const maxX = _.max(creeps, c => c.pos.x);
		const minY = _.min(creeps, c => c.pos.y);
		const maxY = _.max(creeps, c => c.pos.y);

		const first = creeps[0].id;
		const last = creeps[creeps.length - 1].id;

		// If moving diagonally we need to adjust direction based on what kind
		// of highway room it is.
		const allowVertical = isCrossroads(this.room.name) || !this.room.name.endsWith('0');
		const allowHorizontal = isCrossroads(this.room.name) || this.room.name.endsWith('0');

		if (allowHorizontal && minX.id === first && maxX.id === last) return LEFT;
		if (allowHorizontal && maxX.id === first && minX.id === last) return RIGHT;
		if (allowVertical && minY.id === first && maxY.id === last) return TOP;
		if (allowVertical && maxY.id === first && minY.id === last) return BOTTOM;

		return null;
	}

	getTraversedRooms(direction: TOP | BOTTOM | LEFT | RIGHT, creeps: Creep[], firstSeen: number): Array<{name: string; time: number}> {
		const rooms: Array<{name: string; time: number}> = [];

		rooms.push({
			name: this.room.name,
			time: Game.time,
		});

		const skipFirstCrossroads = isCrossroads(this.room.name) && Game.time - firstSeen < 75;
		// @todo Estimate how far caravan needs to travel to room edge.
		let nextTime = Game.time + 50;
		let roomName = this.room.name;
		while (!isCrossroads(roomName) || (roomName === this.room.name && skipFirstCrossroads)) {
			const roomIntel = getRoomIntel(roomName);
			const exits = roomIntel.getAge() < 10_000 ? roomIntel.getExits() : Game.map.describeExits(roomName);
			if (!exits[direction]) break;

			roomName = exits[direction];
			rooms.push({
				name: roomName,
				time: nextTime,
			});
			nextTime += 99 - creeps.length;
		}

		return rooms;
	}

	getStoreContents(creeps: Creep[]): Record<string, number> {
		const result: Record<string, number> = {};

		for (const creep of creeps) {
			for (const resourceType in creep.store) {
				result[resourceType] = (result[resourceType] || 0) + (creep.store[resourceType] || 0);
			}
		}

		return result;
	}

	pruneOldCaravans() {
		cache.inHeap('pruneOldCaravans', 1000, () => {
			const caravanMemory = Memory?.strategy?.caravans || {};
			for (const id in caravanMemory) {
				if (caravanMemory[id].expires < Game.time - 2500) {
					// This caravan is long gone. No need to keep it in memory.
					delete caravanMemory[id];
				}
			}
		});
	}
}
